package com.powernode.ssm.task;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.StrUtil;
import com.powernode.ssm.service.DatabaseMetaDataService;
import com.powernode.ssm.service.SqlGeneratorService;
import com.powernode.ssm.utils.DynamicDataSourceUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.File;
import java.sql.*;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Date;
import java.util.stream.Collectors;

/**
 * 定时任务：用于周期性地导出数据库中的表结构（DDL）与表数据（INSERT SQL）。
 *
 * 主要功能包括：
 * 1. 支持多数据源配置，每个数据源独立导出。
 * 2. 按照设定的时间周期（如每分钟）自动触发导出任务。
 * 3. 遍历每个数据源下的所有表，依次处理。
 * 4. 对每张表，先导出其表结构（CREATE DDL），保存为 _结构.sql 文件。
 * 5. 再根据配置的分页方式（传统分页或Keyset分页），导出表数据，保存为多个 _数据_第N部分.sql 文件。
 * 6. 支持配置每文件最大行数、导出总行数限制等参数。
 *
 * 导出策略：
 * - 传统分页：使用 LIMIT + OFFSET（MySQL等）或 ROW_NUMBER() OVER (Oracle) 实现。
 * - Keyset分页：基于主键字段进行高效分页，避免深分页性能问题，支持复合主键。
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class TableExportScheduledTask {

    private static final int MAX_JDBC_BATCH_PARAMS = 32767;

    @Value("${export.paginationType}")
    private String paginationType; // 分页类型：new（Keyset分页） 或 old（传统分页）

    @Value("${export.backupSQL}")
    private String exportPath; // 导出文件存储的根路径，如 D:/sql_exports

    @Value("${export.backupSQLMaxRows:100}")
    private int maxExportRows; // 每张表最大导出行数，默认100，-1表示导出全部

    @Value("${export.rowsPerFile:1000}")
    private int rowsPerFile; // 每个数据文件最多包含的INSERT语句行数

    private final DynamicDataSourceUtil dynamicDataSourceUtil; // 动态数据源工具，用于获取不同数据源的连接
    private final DatabaseMetaDataService metaDataService; // 提供表结构、主键、列信息等元数据服务
    private final SqlGeneratorService sqlGeneratorService; // 用于生成DDL和DML（INSERT）SQL语句

    // 每天凌晨1点执行（cron表达式）
//    每小时执行一次："0 0 * * * ?"
//    每 30 分钟执行一次："0 */30 * * * ?"
//    每天中午 12 点："0 0 12 * * ?"
//    每 1 分钟执行一次："0 */1 * * * ?"
    @Scheduled(cron = "0 0 2 * * ?")
    public void exportTables() {
        log.info("===== 开始执行表结构与数据导出任务 =====");
        long startTime = System.currentTimeMillis();

        try {
            // 获取所有已配置的数据源名称
            Set<String> dataSourceNames = dynamicDataSourceUtil.getDataSourceNames();
            if (dataSourceNames.isEmpty()) {
                log.warn("未配置任何数据源，任务终止");
                return;
            }

            // 创建按日期组织的导出根目录，如 /20250921
            String dateDir = new SimpleDateFormat("yyyyMMdd").format(new Date());
            String exportRoot = StrUtil.format("{}/{}", exportPath, dateDir);
            FileUtil.mkdir(exportRoot);

            // 遍历每个数据源执行导出任务
            for (String dsName : dataSourceNames) {
                log.info("开始处理数据源: {}", dsName);

                // 获取该数据源的数据库驱动类，并解析出数据库类型（mysql/oracle/postgresql）
                String driverClass = dynamicDataSourceUtil.getDriverClassName(dsName);
                String dbType = parseDbType(driverClass);
                if (StrUtil.isBlank(dbType)) {
                    log.warn("无法识别的数据库驱动: {}, 跳过该数据源", driverClass);
                    continue;
                }

                // 获取该数据源下的所有表名
                List<String> tableNames = metaDataService.getTableNames(dsName);
                if (tableNames.isEmpty()) {
                    log.info("数据源 {} 中无表，跳过", dsName);
                    continue;
                }

                // 为当前数据源创建专属导出目录，如 /20250921/mybatis
                String dsExportDir = StrUtil.format("{}/{}", exportRoot, dsName);
                FileUtil.mkdir(dsExportDir);

                // 遍历该数据源下的每张表，执行导出
                for (String tableName : tableNames) {
                    processTable(dsName, dbType, tableName, dsExportDir);
                }
                log.info("数据源 {} 处理完成，共处理 {} 张表", dsName, tableNames.size());
            }

            long cost = System.currentTimeMillis() - startTime;
            log.info("===== 导出任务完成，总耗时: {}ms =====", cost);
        } catch (Exception e) {
            log.error("导出任务执行失败", e);
        }
    }

    /**
     * 根据字段数量，计算每次JDBC批量操作时合理的参数数量上限，避免超过MAX_JDBC_BATCH_PARAMS
     */
    public static int getDynamicBatchSize(int fieldCount) {
        if (fieldCount <= 0) {
            return 100; // 默认安全值
        }
        int calculated = MAX_JDBC_BATCH_PARAMS / fieldCount;
        return Math.max(1, calculated); // 至少为1
    }

    /**
     * 处理单张表的数据导出任务，包括：导出表结构DDL、导出表数据（根据分页策略）
     */
    private void processTable(String dsName, String dbType, String tableName, String baseDir) {
        try {
            // ================================
            // 1. 获取表元数据 & 主键字段列表
            // ================================
            List<DatabaseMetaDataService.ColumnInfo> columns = metaDataService.getColumnInfos(dsName, tableName);
            if (CollUtil.isEmpty(columns)) {
                log.warn("表 [{}] 无列信息，跳过", tableName);
                return;
            }

            // =============================
            // ⚠️ 防御性处理：对字段列表按字段名去重，避免元数据返回重复字段导致 DDL 异常
            // =============================
            // 去重逻辑：根据字段名去重，若字段名相同，保留第一个出现的字段（通常元数据顺序是可靠的）
            Map<String, DatabaseMetaDataService.ColumnInfo> uniqueColumnsMap = new LinkedHashMap<>();
            for (DatabaseMetaDataService.ColumnInfo column : columns) {
                String fieldName = column.getName();
                if (!uniqueColumnsMap.containsKey(fieldName)) {
                    uniqueColumnsMap.put(fieldName, column);
                } else {
                    log.warn("表 [{}] 中检测到重复字段 [{}]，已自动去重（保留第一个出现的定义）", tableName, fieldName);
                }
            }

            // 转回 List（保持字段顺序，且无重复）
            List<DatabaseMetaDataService.ColumnInfo> deduplicatedColumns = new ArrayList<>(uniqueColumnsMap.values());

            // 只取主键字段（用于排序或主键分页，必须按顺序！）
            List<String> primaryKeyFields = deduplicatedColumns.stream()
                    .filter(DatabaseMetaDataService.ColumnInfo::isPrimaryKey)
                    .map(DatabaseMetaDataService.ColumnInfo::getName)
                    .collect(Collectors.toList());

            log.info("表 [{}] 的主键字段为: {}", tableName, primaryKeyFields);

            // 生成并导出 DDL（使用去重后的字段列表）
            String ddl = sqlGeneratorService.generateCreateDdl(
                    deduplicatedColumns.toArray(new DatabaseMetaDataService.ColumnInfo[0]),
                    dbType,
                    tableName
            );

            String ddlPath = StrUtil.format("{}/{}_结构.sql", baseDir, tableName);
            FileUtil.writeUtf8String(ddl, ddlPath);
            log.info("表 [{}] 的 DDL 已导出到: {}", tableName, ddlPath);

            // ================================
            // 2. 获取总行数，设置导出条数限制
            // ================================
            String countSql = "SELECT COUNT(1) FROM " + tableName;
            int total = executeCount(countSql, dsName);
            int exportLimit = maxExportRows == -1 ? Integer.MAX_VALUE : Math.min(maxExportRows, total);

            log.info("表 [{}] 准备导出数据，总行数: {}, 导出限制: {}", tableName, total, exportLimit);

            if (exportLimit <= 0) {
                log.info("表 [{}] 导出条数限制 <= 0，跳过", tableName);
                return;
            }

            // ================================
            // 3. 根据配置选择分页方式：new（Keyset分页） or old（传统分页）
            // ================================
            if ("new".equalsIgnoreCase(paginationType) && !primaryKeyFields.isEmpty()) {
                // 使用Keyset分页（基于主键的高效分页方式）,并且至少有一个主键字段
                processTableWithKeysetPagination(dsName, dbType, tableName, baseDir, deduplicatedColumns, primaryKeyFields, exportLimit);
            } else {
                // 使用传统分页（LIMIT + OFFSET 或 Oracle ROW_NUMBER）
                processTableWithTraditionalPagination(dsName, dbType, tableName, baseDir, deduplicatedColumns, exportLimit);
            }

        } catch (Exception e) {
            log.error("处理表 [{}] 失败", tableName, e);
        }
    }

    // --------------------------------------------------
    // 方法 1：传统分页（LIMIT + OFFSET / Oracle ROW_NUMBER）
    // 支持：MySQL / PostgreSQL / Oracle
    // 排序：优先主键字段，其次第一个字段
    // --------------------------------------------------
    private void processTableWithTraditionalPagination(
            String dsName,
            String dbType,
            String tableName,
            String baseDir,
            List<DatabaseMetaDataService.ColumnInfo> columns,
            int exportLimit
    ) throws Exception {
        int filePart = 1;
        int exportedCount = 0;
        List<Object[]> batchBuffer = new ArrayList<>(Math.min(rowsPerFile, getDynamicBatchSize(columns.size())));

        final int fetchSize = Math.min(getDynamicBatchSize(columns.size()), exportLimit);

        // 排序字段逻辑：优先主键，否则第一个字段
        String orderByClause;
        List<String> primaryKeyFields = columns.stream()
                .filter(DatabaseMetaDataService.ColumnInfo::isPrimaryKey)
                .map(DatabaseMetaDataService.ColumnInfo::getName)
                .collect(Collectors.toList());

        if (CollUtil.isNotEmpty(primaryKeyFields)) {
            orderByClause = primaryKeyFields.stream().map(f -> f).collect(Collectors.joining(", "));
            log.info("表 [{}] 使用主键字段 [{}] 作为排序字段进行传统分页", tableName, orderByClause);
        } else {
            String firstColumn = columns.get(0).getName();
            orderByClause = firstColumn;
            log.info("表 [{}] 无主键，使用第一个字段 [{}] 作为排序字段进行传统分页", tableName, firstColumn);
        }

        int offset = 0;

        while (exportedCount < exportLimit) {
            String dataSql;

            if ("oracle".equalsIgnoreCase(dbType)) {
                // Oracle 分页：使用 ROW_NUMBER() OVER (ORDER BY ...) AS RN，然后筛选 RN BETWEEN offset+1 AND offset+fetchSize
                dataSql = String.format(
                        "SELECT * FROM (" +
                                "   SELECT t.*, ROW_NUMBER() OVER (ORDER BY %s) AS RN FROM %s t" +
                                ") WHERE RN BETWEEN %d AND %d",
                        orderByClause,
                        tableName,
                        offset + 1,
                        offset + fetchSize
                );
            } else {
                // MySQL / PostgreSQL 等：直接使用 LIMIT fetchSize OFFSET offset
                dataSql = String.format(
                        "SELECT * FROM %s ORDER BY %s LIMIT %d OFFSET %d",
                        tableName,
                        orderByClause,
                        fetchSize,
                        offset
                );
            }

            log.info("表 [{}] 生成的传统分页 SQL: {}", tableName, dataSql);

            List<Object[]> dataList = executeQuery(dataSql, dsName);

            if (CollUtil.isEmpty(dataList)) {
                break;
            }

            for (Object[] row : dataList) {
                if (exportedCount >= exportLimit) break;
                batchBuffer.add(row);
                exportedCount++;

                if (batchBuffer.size() >= rowsPerFile) {
                    String insertSql = sqlGeneratorService.generateInsertDml(
                            tableName,
                            batchBuffer,
                            columns.toArray(new DatabaseMetaDataService.ColumnInfo[0]),
                            dbType
                    );
                    String filePath = StrUtil.format("{}/{}_数据_第{}部分.sql", baseDir, tableName, filePart);
                    FileUtil.writeUtf8String(insertSql, filePath);
                    filePart++;
                    batchBuffer.clear();
                }
            }

            if (dataList.size() < fetchSize) {
                break;
            }

            offset += fetchSize;
        }

        if (!batchBuffer.isEmpty()) {
            String insertSql = sqlGeneratorService.generateInsertDml(
                    tableName,
                    batchBuffer,
                    columns.toArray(new DatabaseMetaDataService.ColumnInfo[0]),
                    dbType
            );
            String filePath = StrUtil.format("{}/{}_数据_第{}部分.sql", baseDir, tableName, filePart);
            FileUtil.writeUtf8String(insertSql, filePath);
        }

        log.info("✅ 表 [{}]（传统分页）数据导出完成，共导出 {} 条，分为 {} 个文件", tableName, exportedCount, filePart);
    }

    // --------------------------------------------------
    // 方法 2：主键分页（keyset pagination）—— 通用适配所有字段数量
    // 支持：Oracle / MySQL / PostgreSQL
    // 每次根据上一页最后一条数据的主键值，构造动态条件：
    // (k1 > ?) OR (k1 = ? AND k2 > ?) OR (k1 = ? AND k2 = ? AND k3 > ?) ...
    // --------------------------------------------------
    private void processTableWithKeysetPagination(
            String dsName,
            String dbType,
            String tableName,
            String baseDir,
            List<DatabaseMetaDataService.ColumnInfo> columns,
            List<String> primaryKeyFields,
            int exportLimit
    ) throws Exception {
        int filePart = 1;
        int exportedCount = 0;
        List<Object[]> batchBuffer = new ArrayList<>(Math.min(rowsPerFile, getDynamicBatchSize(columns.size())));

        // 每次从数据库获取的批量大小，不超过 exportLimit 和单批次大小
        final int fetchSize = Math.min(getDynamicBatchSize(columns.size()), exportLimit);

        // 是否为第一页（第一页不使用 keyset 条件）
        boolean isFirstPage = true;

        // 用于存储上一页最后一条记录的主键值，从第二页开始使用
        List<Object> lastRowValues = new ArrayList<>();

        while (exportedCount < exportLimit) {
            // 1. 动态构建 WHERE 条件（仅第 2 页及以后使用 keyset 条件，如 (id > ?) 或 (id = ? AND str > ?) ...）
            String whereClause = isFirstPage ? "" : buildDynamicKeysetWhereClause(primaryKeyFields);

            // 2. 构建完整的分页 SQL，按主键字段排序
            String dataSql = String.format(
                    "SELECT * FROM %s %s ORDER BY %s LIMIT %d",
                    tableName,
                    whereClause.isEmpty() ? "" : "WHERE " + whereClause,
                    String.join(", ", primaryKeyFields), // 按主键字段排序
                    fetchSize
            );

            log.info("表 [{}] 生成的主键分页 SQL: {}", tableName, dataSql);

            // 3. 构建动态参数列表：仅第 2 页及以后传入 lastRowValues 中的主键值
            List<Object> params;
            if (isFirstPage) {
                params = Collections.emptyList(); // 第一页无参数
            } else {
                params = buildDynamicKeysetPaginationParams(primaryKeyFields, lastRowValues);
            }

            log.info("【DEBUG】表 [{}] 执行分页 SQL: {}, 参数个数: {}, 参数值: {}",
                    tableName, dataSql, params.size(), params);

            // 4. 执行查询
            List<Object[]> dataList = executeQuery(dataSql, dsName, params.toArray());

            // 5. 如果当前页无数据，则退出循环
            if (CollUtil.isEmpty(dataList)) {
                break;
            }

            // 6. 遍历当前页数据，写入导出文件
            for (Object[] row : dataList) {
                if (exportedCount >= exportLimit) {
                    break;
                }
                batchBuffer.add(row);
                exportedCount++;

                // 达到单文件行数限制时，写入文件并清空 buffer
                if (batchBuffer.size() >= rowsPerFile) {
                    String insertSql = sqlGeneratorService.generateInsertDml(
                            tableName,
                            batchBuffer,
                            columns.toArray(new DatabaseMetaDataService.ColumnInfo[0]),
                            dbType
                    );
                    String filePath = StrUtil.format("{}/{}_数据_第{}部分.sql", baseDir, tableName, filePart);
                    FileUtil.writeUtf8String(insertSql, filePath);
                    filePart++;
                    batchBuffer.clear();
                }
            }

            // 7. 判断是否还有下一页数据
            if (dataList.size() < fetchSize) {
                // 当前页数据不足fetchSize，说明已经是最后一页
                break;
            } else {
                // 当前页数据等于fetchSize，说明可能还有下一页
                // 提取当前页最后一条记录的主键值，作为下一页的起始条件
                Object[] lastRow = dataList.get(dataList.size() - 1);
                lastRowValues.clear();

                for (String pkField : primaryKeyFields) {
                    int idx = getColumnIndexByName(columns, pkField);
                    if (idx == -1 || lastRow.length <= idx) {
                        throw new IllegalStateException(String.format(
                                "主键字段 '%s' 在表 [%s] 的结果集中找不到对应索引，请检查元数据一致性", pkField, tableName
                        ));
                    }
                    lastRowValues.add(lastRow[idx]);
                }

                log.info("🔁 [Keyset] 表 [{}] 下一页的 lastRowValues = {}", tableName, lastRowValues);

                // 下一页不再是第一页
                isFirstPage = false;
            }
        }

        // 8. 写入剩余的最后一批数据（如果有）
        if (!batchBuffer.isEmpty()) {
            String insertSql = sqlGeneratorService.generateInsertDml(
                    tableName,
                    batchBuffer,
                    columns.toArray(new DatabaseMetaDataService.ColumnInfo[0]),
                    dbType
            );
            String filePath = StrUtil.format("{}/{}_数据_第{}部分.sql", baseDir, tableName, filePart);
            FileUtil.writeUtf8String(insertSql, filePath);
        }

        log.info("✅ 表 [{}]（主键分页）数据导出完成，共导出 {} 条，分为 {} 个文件", tableName, exportedCount, filePart);
    }

    /**
     * 动态构建 keyset 分页 WHERE 条件，支持 N 个主键字段：
     * 例如：
     * - 单字段：(id > ?)
     * - 双字段：(id > ?) OR (id = ? AND str > ?)
     * - 三字段：(id > ?) OR (id = ? AND str > ?) OR (id = ? AND str = ? AND date > ?)
     * - N 字段：依次类推，每多一个字段，多一层 AND ... = ? 条件
     */
    private String buildDynamicKeysetWhereClause(List<String> primaryKeyFields) {
        if (primaryKeyFields == null || primaryKeyFields.isEmpty()) {
            return "";
        }
        String tupleFields = primaryKeyFields.stream().collect(Collectors.joining(", "));
        String placeholders = primaryKeyFields.stream().map(f -> "?").collect(Collectors.joining(", "));
        return String.format("(%s) > (%s)", tupleFields, placeholders);
    }

    /**
     * 动态构建 keyset 分页查询参数，支持 N 个主键字段
     * 参数顺序与 buildDynamicKeysetWhereClause 生成的 ? 顺序完全一致：
     * 比如：
     * - 单字段：[idVal]
     * - 双字段：[idVal, strVal]
     * - 三字段：[idVal, strVal, dateVal]
     * - N 字段：同理逐层构造
     */
    private List<Object> buildDynamicKeysetPaginationParams(
            List<String> primaryKeyFields,
            List<Object> lastRowValues
    ) {
        List<Object> params = new ArrayList<>();

        if (lastRowValues == null || lastRowValues.isEmpty()) {
            log.debug("构建 Keyset 参数：lastRowValues 为空，返回空参数列表");
            return params;
        }

        // 按照主键字段顺序，依次添加每个字段的值
        for (int i = 0; i < primaryKeyFields.size(); i++) {
            Object value = lastRowValues.get(i);
            params.add(value);
        }

        log.debug("构建的 Keyset 分页参数，主键字段数量: {}, 参数个数: {}, 参数值: {}",
                primaryKeyFields.size(), params.size(), params);
        return params;
    }

    /**
     * 根据字段名获取其在 columns 列表中的索引
     */
    private int getColumnIndexByName(List<DatabaseMetaDataService.ColumnInfo> columns, String fieldName) {
        for (int i = 0; i < columns.size(); i++) {
            if (columns.get(i).getName().equals(fieldName)) {
                return i;
            }
        }
        return -1;
    }

    /**
     * 执行带参数的查询SQL（支持 ? 占位符），返回结果集 Object[] 行列表
     *
     * @param sql    带 ? 占位符的 SQL 语句，如 "SELECT * FROM tbl WHERE id > ? ORDER BY id ASC"
     * @param dsName 数据源名称
     * @param params 对应 ? 的参数值，如 new Object[]{100}，按顺序绑定
     * @return 查询结果的每一行是一个 Object[]，按列顺序
     * @throws Exception
     */
    private List<Object[]> executeQuery(String sql, String dsName, Object... params) throws Exception {
        log.debug("【executeQuery】执行 SQL: {}, 参数个数: {}, 参数内容: {}", sql, params.length, Arrays.toString(params));

        List<Object[]> result = new ArrayList<>();
        try (
                Connection conn = dynamicDataSourceUtil.getConnection(dsName);
                // ✅ 使用 PreparedStatement，而不是 Statement
                PreparedStatement pstmt = conn.prepareStatement(sql)
        ) {
            // ✅ 绑定参数（按顺序）
            for (int i = 0; i < params.length; i++) {
                pstmt.setObject(i + 1, params[i]); // JDBC 参数索引从 1 开始
            }

            // 执行查询
            try (ResultSet rs = pstmt.executeQuery()) {
                ResultSetMetaData metaData = rs.getMetaData();
                int columnCount = metaData.getColumnCount();

                while (rs.next()) {
                    Object[] row = new Object[columnCount];
                    for (int i = 1; i <= columnCount; i++) {
                        row[i - 1] = rs.getObject(i); // 列索引从 1 开始
                    }
                    result.add(row);
                }
            }
        }
        return result;
    }

    /**
     * 执行COUNT查询
     */
    private int executeCount(String sql, String dsName) throws Exception {
        try (Connection conn = dynamicDataSourceUtil.getConnection(dsName);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(sql)) {

            return rs.next() ? rs.getInt(1) : 0;
        }
    }

    /**
     * 写入文件（UTF-8编码）
     */
    private void writeToFile(String filePath, String content) {
        try {
            File file = new File(filePath);
            FileUtil.writeUtf8String(content, file);
            log.debug("文件已生成: {}", filePath);
        } catch (Exception e) {
            log.error("写入文件失败: {}", filePath, e);
        }
    }

    /**
     * 根据驱动类解析数据库类型
     */
    private String parseDbType(String driverClass) {
        if (StrUtil.containsAny(driverClass, "mysql")) return "mysql";
        if (StrUtil.containsAny(driverClass, "oracle")) return "oracle";
        if (StrUtil.containsAny(driverClass, "postgresql")) return "postgresql";
        return StrUtil.EMPTY;
    }
}